package uploader

import (
	"bytes"
	"client/common/response"
	"client/file"
	"client/services"
	"client/utils"
	"context"
	"encoding/json"
	"fmt"
	"github.com/google/uuid"
	"io"
	"math"
	"mime/multipart"
	"net/http"
	"net/url"
	"os"
	"path/filepath"
	"strconv"
	"sync"
)

type FragmentUploadConfig struct {
	FileMetaDir string
	SliceSize   int
	FileMaxSize int64
	FilePath    string
}
type FragmentUploader struct {
	FragmentUploadConfig
	*file.FileMeta
	needUploadSlice []int
	client          http.Client
	retryChan       chan *file.FilePart
	waitGroup       *sync.WaitGroup
}

func (d *FragmentUploader) uploadSlice(c context.Context, filePart *file.FilePart) {
	d.waitGroup.Add(1)
	defer d.waitGroup.Done()

	bs := &bytes.Buffer{}
	writer := multipart.NewWriter(bs)

	part, err := writer.CreateFormFile("file", strconv.Itoa(filePart.Idx))
	if err != nil {
		fmt.Println(err)
		return
	}
	_, err = io.Copy(part, bytes.NewReader(filePart.Data))
	if err != nil {
		fmt.Println(err)
		return
	}
	err = writer.WriteField("md5", filePart.Md5)
	if err != nil {
		fmt.Println(err)
		return
	}
	err = writer.WriteField("idx", strconv.Itoa(filePart.Idx))
	if err != nil {
		fmt.Println(err)
		return
	}
	err = writer.WriteField("fid", filePart.Fid)
	if err != nil {
		fmt.Println(err)
		return
	}
	err = writer.Close()
	if err != nil {
		fmt.Println(err)
	}

	var req *http.Request
	req, err = http.NewRequestWithContext(c, http.MethodPost, services.UploadApiInstance.UploadSlice, bs)
	if err != nil {
		fmt.Println(err)
		return
	}
	req.Header.Set("Content-Type", writer.FormDataContentType())

	var res *http.Response
	res, err = d.client.Do(req)
	if err != nil {
		// 若上传失败则放入retryChan中等待重新上传
		d.retryChan <- filePart
		fmt.Println(err)
		return
	}

	_, err = response.ParseResponse(res)
	if err != nil {
		d.retryChan <- filePart
		fmt.Println(err)
		return
	}
	fmt.Printf("切片 %d 上传完成\n", filePart.Idx)
}

func (d *FragmentUploader) handleRetry(c context.Context) {
	for {
		select {
		case filePart := <-d.retryChan:
			// 注意：waitGroup写的有漏洞，这里有的信号没有处理完，wait()就返回了
			fmt.Println("重传 filePart, idx: ", filePart.Idx)
			go d.uploadSlice(c, filePart)
		case <-c.Done():
			fmt.Println(c.Err())
			panic("timeout")
		}
	}
}

func (d *FragmentUploader) UploadSlices(c context.Context) {
	fl, err := os.Open(d.FilePath)
	defer fl.Close()
	if err != nil {
		fmt.Println(err)
		return
	}
	go func() {
		d.handleRetry(c)
	}()

	for _, num := range d.needUploadSlice {
		_, err = fl.Seek(int64(d.SliceSize*num), io.SeekStart)
		if err != nil {
			fmt.Println(err)
			return
		}
		// 注意： 这里不能写0 data := make([]byte,0,d.SliceSize), 不然读不出来数据
		data := make([]byte, d.SliceSize)
		var length int
		length, err = fl.Read(data)
		if err != nil {
			fmt.Println(err)
			return
		}
		// 因为最后一个片段大小一般小于sliceSize
		data = data[:length]
		var md5Str string
		md5Str, err = utils.GetMd5String(data)
		if err != nil {
			fmt.Println(err)
			return
		}
		filePart := &file.FilePart{
			Fid:  d.Fid,
			Idx:  num,
			Data: data,
			Md5:  md5Str,
		}
		go d.uploadSlice(c, filePart)
	}
}

func (d *FragmentUploader) handleFileMeta(info os.FileInfo) {
	filename := info.Name()
	// 客户端使用filename作为元数据文件标识，服务端使用fid作为文件唯一标识
	metaPath := filepath.Join(d.FileMetaDir, filename+"-file_meta.json")
	fileMeta, err := file.GetFileMeta(metaPath)
	if err != nil {
		fmt.Println(err)
		fmt.Println("未获取到fileMeta文件，将创建新的meta")
		var md5Str string
		md5Str, err = utils.GetMd5StringByFilePath(d.FilePath)
		if err != nil {
			fmt.Println(err)
			return
		}
		size := info.Size()
		fileMeta = &file.FileMeta{
			Fid:        uuid.New().String(),
			Name:       info.Name(),
			Size:       size,
			SliceNum:   int(math.Ceil(float64(size) / float64(d.SliceSize))),
			ModifyTime: info.ModTime(),
			Md5:        md5Str,
		}
		err = file.SetFileMeta(metaPath, fileMeta)
		if err != nil {
			// 设置失败影响不大，所以err不需要返回
			fmt.Println(err)
		}
	} else {
		if fileMeta.ModifyTime != info.ModTime() || fileMeta.Size != info.Size() {
			fmt.Println("获取到fileMeta文件，meta信息有变化，将创建新的meta")
			var md5Str string
			md5Str, err = utils.GetMd5StringByFilePath(d.FilePath)
			if err != nil {
				fmt.Println(err)
				return
			}
			size := info.Size()
			fileMeta = &file.FileMeta{
				Fid:        uuid.New().String(),
				Name:       info.Name(),
				Size:       size,
				SliceNum:   int(math.Ceil(float64(size) / float64(d.SliceSize))),
				ModifyTime: info.ModTime(),
				Md5:        md5Str,
			}
			err = file.SetFileMeta(metaPath, fileMeta)
			if err != nil {
				// 设置失败影响不大，所以err不需要返回
				fmt.Println(err)
			}
		}
	}
	d.FileMeta = fileMeta
}

func (d *FragmentUploader) handlePrepareUpload(c context.Context) {
	//	准备工作：通知服务端即将开始上传文件分片
	//		服务端检测对应文件meta信息是否存在，
	//			若存在，则比对meta，若meta比对一致，表明文件上传过但未完成，则返回需要重传的切片序号
	//			若meta不存在或者meta比对不一致，则保存meta信息，创建文件夹用于保存分片信息

	bs, err := json.Marshal(&d.FileMeta)
	if err != nil {
		fmt.Println(err)
		return
	}
	var req *http.Request
	req, err = http.NewRequestWithContext(c, http.MethodPost, services.UploadApiInstance.PrepareUpload, bytes.NewReader(bs))
	if err != nil {
		fmt.Println(err)
		return
	}
	var res *http.Response
	res, err = d.client.Do(req)
	if err != nil {
		fmt.Println(err)
		return
	}

	var resp *response.Response
	resp, err = response.ParseResponse(res)
	if err != nil {
		fmt.Println(err)
		return
	}

	var needUploadSlice []int
	needUploadSlice, err = resp.GetRespSliceInt()
	if err != nil {
		fmt.Println(err)
		return
	}

	if len(needUploadSlice) == 0 {
		fmt.Println("服务端通知所有分片已上传完成，可通知handleFinished")
		d.needUploadSlice = []int{}
		return
	}

	if len(needUploadSlice) == 1 && needUploadSlice[0] == -1 {
		slice := make([]int, 0, d.SliceNum)
		for i := 0; i < d.SliceNum; i++ {
			slice = append(slice, i)
		}
		d.needUploadSlice = slice
	} else {
		d.needUploadSlice = needUploadSlice
	}
	fmt.Println("待上传分片：", d.needUploadSlice)
}

func (d *FragmentUploader) handleUploadFinished(c context.Context) {
	params := url.Values{}
	params.Set("fid", d.Fid)
	fullUrl := fmt.Sprintf("%s?%s", services.UploadApiInstance.UploadFinished, params.Encode())
	req, err := http.NewRequestWithContext(c, http.MethodGet, fullUrl, nil)
	if err != nil {
		fmt.Println(err)
		return
	}
	var res *http.Response
	res, err = d.client.Do(req)
	if err != nil {
		fmt.Println(err)
		return
	}

	var resp *response.Response
	resp, err = response.ParseResponse(res)
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println(resp.Data)
	metaPath := filepath.Join(d.FileMetaDir, d.Name+"-file_meta.json")
	err = os.Remove(metaPath)
	if err != nil {
		fmt.Println(err)
		return
	}
}

func (d *FragmentUploader) HandleUpload(c context.Context) {
	info, err := utils.FileExist(d.FilePath)
	if err != nil {
		fmt.Println(err)
		return
	}
	// 获取文件meta信息
	d.handleFileMeta(info)
	// 准备上传文件，确认需要上传的分片
	d.handlePrepareUpload(c)
	if len(d.needUploadSlice) != 0 {
		// 上传分片
		d.UploadSlices(c)
		d.waitGroup.Wait()
	}
	// 上传完成，通知服务端检查文件完整性
	d.handleUploadFinished(c)
}

func NewFragmentUploader(config FragmentUploadConfig, client http.Client) *FragmentUploader {
	return &FragmentUploader{
		FragmentUploadConfig: config,
		client:               client,
		retryChan:            make(chan *file.FilePart, 3),
		waitGroup:            &sync.WaitGroup{},
	}
}
